{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 导入椭圆曲线算法\n",
    "from ecdsa import SigningKey, SECP256k1, VerifyingKey, BadSignatureError\n",
    "import binascii\n",
    "import base64\n",
    "from hashlib import sha256\n",
    "\n",
    "\n",
    "class Wallet:\n",
    "    \"\"\"\n",
    "        钱包\n",
    "    \"\"\"\n",
    "    def __init__(self):\n",
    "        \"\"\"\n",
    "            钱包初始化时基于椭圆曲线生成一个唯一的秘钥对，代表区块链上一个唯一的账户\n",
    "        \"\"\"\n",
    "        self._private_key = SigningKey.generate(curve=SECP256k1)\n",
    "        self._public_key = self._private_key.get_verifying_key()\n",
    "\n",
    "    @property\n",
    "    def address(self):\n",
    "        \"\"\"\n",
    "            这里通过公钥生成地址\n",
    "        \"\"\"\n",
    "        h = sha256(self._public_key.to_pem())\n",
    "        return base64.b64encode(h.digest())\n",
    "\n",
    "    @property\n",
    "    def pubkey(self):\n",
    "        \"\"\"\n",
    "            返回公钥字符串\n",
    "        \"\"\"\n",
    "        return self._public_key.to_pem()\n",
    "\n",
    "    def sign(self, message):\n",
    "        \"\"\"\n",
    "            生成数字签名\n",
    "        \"\"\"\n",
    "        h = sha256(message.encode('utf8'))\n",
    "        return binascii.hexlify(self._private_key.sign(h.digest()))\n",
    "\n",
    "    \n",
    "def verify_sign(pubkey, message, signature):\n",
    "    \"\"\"\n",
    "        验证签名\n",
    "    \"\"\"\n",
    "    verifier = VerifyingKey.from_pem(pubkey)\n",
    "    h = sha256(message.encode('utf8'))\n",
    "    return verifier.verify(binascii.unhexlify(signature), h.digest())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "\n",
    "class Transaction:\n",
    "    \"\"\"\n",
    "    交易的结构\n",
    "    \"\"\"\n",
    "    def __init__(self, sender, recipient, amount):\n",
    "        \"\"\"\n",
    "            初始化交易，设置交易的发送方、接收方和交易数量\n",
    "        \"\"\"\n",
    "        if isinstance(sender, bytes):\n",
    "            sender = sender.decode('utf-8')\n",
    "        self.sender = sender            # 发送方\n",
    "        if isinstance(recipient, bytes):\n",
    "            recipient = recipient.decode('utf-8')\n",
    "        self.recipient = recipient      # 接收方\n",
    "        self.amount = amount            # 交易数量\n",
    "        \n",
    "    def set_sign(self, signature, pubkey):\n",
    "        \"\"\"\n",
    "            为了便于验证这个交易的可靠性，需要发送方输入他的公钥和签名\n",
    "        \"\"\"\n",
    "        self.signature = signature      # 签名\n",
    "        self.pubkey = pubkey            # 发送方公钥\n",
    "        \n",
    "    def __repr__(self):\n",
    "        \"\"\"\n",
    "            交易大致可分为两种，一是挖矿所得，而是转账交易\n",
    "            挖矿所得无发送方，以此进行区分显示不同内容\n",
    "        \"\"\"\n",
    "        if self.sender:\n",
    "            s = \"从 %s 转至 %s %d个加密货币\" % (self.sender, self.recipient, self.amount)\n",
    "        else:\n",
    "            s = \"%s 挖矿获取%d个加密货币\" % (self.recipient, self.amount)\n",
    "        return s\n",
    "\n",
    "\n",
    "class TransactionEncoder(json.JSONEncoder):\n",
    "    \"\"\"\n",
    "    定义Json的编码类，用来序列化Transaction\n",
    "    \"\"\"\n",
    "    def default(self, obj):\n",
    "        if isinstance(obj, Transaction):\n",
    "            return obj.__dict__\n",
    "        else:\n",
    "            return json.JSONEncoder.default(self, obj)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import hashlib\n",
    "from datetime import datetime\n",
    "\n",
    "\n",
    "class Block:\n",
    "    \"\"\"\n",
    "        区块结构\n",
    "            prev_hash:      父区块哈希值\n",
    "            transactions:           交易对\n",
    "            timestamp:      区块创建时间\n",
    "            hash:           区块哈希值\n",
    "            Nonce:        随机数\n",
    "    \"\"\"\n",
    "    def __init__(self, transactions, prev_hash):\n",
    "        # 将传入的父哈希值和数据保存到类变量中\n",
    "        self.prev_hash = prev_hash    \n",
    "        self.transactions = transactions\n",
    "        # 获取当前时间\n",
    "        self.timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n",
    "        \n",
    "        # 设置Nonce和哈希的初始值为None\n",
    "        self.nonce = None\n",
    "        self.hash = None\n",
    "        \n",
    "    def __repr__(self):\n",
    "        return \"区块内容：%s\\n哈希值: %s\" % (json.dumps(self.transactions), self.hash)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "DIFFICULTY = 5\n",
    "\n",
    "class ProofOfWork:\n",
    "    \"\"\"\n",
    "        工作量证明\n",
    "    \"\"\"\n",
    "    def __init__(self, block, miner, difficult=5):\n",
    "        self.block = block\n",
    "        \n",
    "        # 定义工作量难度，默认为5，表示有效的哈希值以5个“0”开头\n",
    "        self.difficulty = DIFFICULTY\n",
    "\n",
    "        self.miner = miner\n",
    "        # 添加挖矿奖励\n",
    "        self.reward_amount = 1\n",
    "\n",
    "    def mine(self):\n",
    "        \"\"\"\n",
    "            挖矿函数\n",
    "        \"\"\"\n",
    "        i = 0\n",
    "        prefix = '0' * self.difficulty\n",
    "        \n",
    "        \n",
    "        # 添加奖励\n",
    "        t = Transaction(\n",
    "                sender=\"\",\n",
    "                recipient=self.miner.address,\n",
    "                amount=self.reward_amount,\n",
    "            )\n",
    "        sig = self.miner.sign(json.dumps(t, cls=TransactionEncoder))\n",
    "        t.set_sign(sig, self.miner.pubkey)\n",
    "        self.block.transactions.append(t)\n",
    "\n",
    "        while True:\n",
    "            message = hashlib.sha256()\n",
    "            message.update(str(self.block.prev_hash).encode('utf-8'))\n",
    "            # 更新区块中的交易数据\n",
    "            # message.update(str(self.block.data).encode('utf-8'))\n",
    "            message.update(str(self.block.transactions).encode('utf-8'))\n",
    "            message.update(str(self.block.timestamp).encode('utf-8'))\n",
    "            message.update(str(i).encode(\"utf-8\"))\n",
    "            digest = message.hexdigest()\n",
    "            if digest.startswith(prefix):\n",
    "                self.block.nonce = i\n",
    "                self.block.hash = digest\n",
    "                return self.block\n",
    "            i += 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "class BlockChain:\n",
    "    \"\"\"\n",
    "        区块链结构体\n",
    "            blocks:        包含的区块列表\n",
    "    \"\"\"\n",
    "    def __init__(self):\n",
    "        self.blocks = []\n",
    "\n",
    "    def add_block(self, block):\n",
    "        \"\"\"\n",
    "        添加区块\n",
    "        \"\"\"\n",
    "        self.blocks.append(block)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "import socket  \n",
    "import threading\n",
    "\n",
    "# 定义一个全局列表保存所有节点\n",
    "NODE_LIST = []\n",
    "\n",
    "class Node(threading.Thread):\n",
    "    def __init__(self, name, port, host=\"localhost\"):\n",
    "        threading.Thread.__init__(self, name=name)\n",
    "        self.host = host       #  服务器地址，本地电脑都设为localhost\n",
    "        self.port = port        # 每个节点对应一个唯一的端口号\n",
    "        self.name = name    # 唯一的节点名称\n",
    "        self.wallet = Wallet()\n",
    "        self.blockchain = None    # 用来存储一个区块链副本\n",
    "        \n",
    "    def run(self):\n",
    "        \"\"\"\n",
    "            节点运行\n",
    "        \"\"\"\n",
    "        self.init_blockchain()    # 初始化区块链\n",
    "        \n",
    "        # 在指定端口进行监听\n",
    "        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  \n",
    "        sock.bind((self.host, self.port))  \n",
    "        NODE_LIST.append({\n",
    "            \"name\": self.name,\n",
    "            \"host\": self.host,\n",
    "            \"port\": self.port\n",
    "        })\n",
    "        sock.listen(10) \n",
    "        print(self.name, \"运行中...\")\n",
    "        while True:       # 不断处理其他节点发送的请求\n",
    "            connection,address = sock.accept()  \n",
    "            try:\n",
    "                print(self.name, \"处理请求内容...\")\n",
    "                self.handle_request(connection)\n",
    "            except socket.timeout:  \n",
    "                print('超时!')\n",
    "            except Exception as e:\n",
    "                print(e, )\n",
    "            connection.close()\n",
    "    \n",
    "    def handle_request(self, connection):\n",
    "        data = []\n",
    "        while True:    # 不断读取请求数据直至读取完成\n",
    "            buf = connection.recv(PER_BYTE)  \n",
    "            if not buf: # 若读取不到新的数据则退出\n",
    "                break\n",
    "            data.append(buf)\n",
    "            if len(buf) < PER_BYTE:   # 若读取到的数据长度小于规定长度，说明数据读取完成，退出\n",
    "                break\n",
    "        t = pickle.loads(b''.join(data))\n",
    "        if isinstance(t, Transaction):  # 如果是新区块类型类型消息\n",
    "            print(\"处理交易请求...\")\n",
    "            if verify_sign(t.pubkey, \n",
    "                  str(t),\n",
    "                   t.signature):\n",
    "    \n",
    "                # 验证交易签名没问题，生成一个新的区块\n",
    "                print(self.name, \"验证交易成功\")\n",
    "                new_block = Block(transactions=[t], prev_hash=\"\")\n",
    "                print(self.name, \"生成新的区块...\")\n",
    "                w = ProofOfWork(new_block, self.wallet)\n",
    "                block = w.mine()\n",
    "                print(self.name, \"将新区块添加到区块链中\")\n",
    "                self.blockchain.add_block(block)\n",
    "                print(self.name, \"将新区块广播到网络中...\")\n",
    "                self.broadcast_new_block(block)\n",
    "            else:\n",
    "                print(self.name, \"交易验证失败！\")\n",
    "        elif isinstance(t, Block):\n",
    "            print(\"处理新区块请求...\")\n",
    "            if self.verify_block(t):\n",
    "                print(self.name, \"区块验证成功\")\n",
    "                self.blockchain.add_block(t)\n",
    "                print(self.name, \"添加新区块成功\")\n",
    "            else:\n",
    "                print(self.name, \"区块验证失败!\")\n",
    "        else:  # 如果不是新区块消息，默认为初始化消息类型，返回本地区块链内容\n",
    "            connection.send(pickle.dumps(self.blockchain))\n",
    "            \n",
    "    def verify_block(self, block):\n",
    "        \"\"\"\n",
    "            验证区块有效性\n",
    "        \"\"\"\n",
    "        message = hashlib.sha256()\n",
    "        message.update(str(block.prev_hash).encode('utf-8'))\n",
    "        # 更新区块中的交易数据\n",
    "        # message.update(str(self.block.data).encode('utf-8'))\n",
    "        message.update(str(block.transactions).encode('utf-8'))\n",
    "        message.update(str(block.timestamp).encode('utf-8'))\n",
    "        message.update(str(block.nonce).encode('utf-8'))\n",
    "        digest = message.hexdigest()\n",
    "\n",
    "        prefix = '0' * DIFFICULTY\n",
    "        return digest.startswith(prefix)\n",
    "            \n",
    "    def broadcast_new_block(self, block):\n",
    "        \"\"\"\n",
    "            将新生成的区块广播到网络中其他节点\n",
    "        \"\"\"\n",
    "        for node in NODE_LIST:\n",
    "            host =node['host']\n",
    "            port = node['port']\n",
    "            \n",
    "            if host == self.host and port == self.port:\n",
    "                print(self.name, \"忽略自身节点\")\n",
    "            else:\n",
    "                print(self.name, \"广播新区块至 %s\" % (node['name']))\n",
    "                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "                sock.connect((host, port))    # 连接到网络中的节点\n",
    "                sock.send(pickle.dumps(block))   # 发送新区块\n",
    "                sock.close()          # 发送完成后关闭连接\n",
    "                \n",
    "    def init_blockchain(self):\n",
    "        \"\"\"\n",
    "            初始化当前节点的区块链\n",
    "        \"\"\"\n",
    "        if NODE_LIST:                # 若当前网络中已存在其他节点，则从第一个节点从获取区块链信息\n",
    "            host = NODE_LIST[0]['host']\n",
    "            port = NODE_LIST[0]['port']\n",
    "            name = NODE_LIST[0][\"name\"]\n",
    "            print(self.name, \"发送初始化请求 %s\" % (name))\n",
    "            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "            sock.connect((host, port))    # 连接到网络中的第一个节点\n",
    "            sock.send(pickle.dumps('INIT'))   # 发送初始化请求\n",
    "            data = []\n",
    "            while True:          # 读取区块链信息，直至完全获取后退出\n",
    "                buf = sock.recv(PER_BYTE)\n",
    "                if not buf: \n",
    "                    break\n",
    "                data.append(buf)\n",
    "                if len(buf) < PER_BYTE:\n",
    "                    break\n",
    "            sock.close()   # 获取完成后关闭连接\n",
    "            \n",
    "            # 将获取的区块链信息赋值到当前节点\n",
    "            self.blockchain = pickle.loads(b''.join(data))\n",
    "            print(self.name, \"初始化完成.\")\n",
    "        else:\n",
    "            # 如果是网络中的第一个节点，初始化一个创世区块\n",
    "            block = Block(transactions=[], prev_hash=\"\")\n",
    "            w = ProofOfWork(block, self.wallet)\n",
    "            genesis_block = w.mine()\n",
    "            self.blockchain = BlockChain()\n",
    "            self.blockchain.add_block(genesis_block)\n",
    "            print(\"生成创世区块\")\n",
    "    \n",
    "    def submit_transaction(self, transaction):\n",
    "         for node in NODE_LIST:\n",
    "            host =node['host']\n",
    "            port = node['port']\n",
    "            \n",
    "            if host == self.host and port == self.port:\n",
    "                print(self.name, \"忽略自身节点\")\n",
    "            else:\n",
    "                print(self.name, \"广播新区块至 %s:%s\" % (self.host, self.port))\n",
    "                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  \n",
    "                sock.connect((node[\"host\"], node[\"port\"]))  \n",
    "                sock.send(pickle.dumps(transaction)) \n",
    "                sock.close()\n",
    "        \n",
    "    def get_balance(self):\n",
    "        balance = 0\n",
    "        for block in self.blockchain.blocks:\n",
    "            for t in block.transactions:\n",
    "                if t.sender == self.wallet.address.decode():\n",
    "                    balance -= t.amount\n",
    "                elif t.recipient == self.wallet.address.decode():\n",
    "                    balance += t.amount\n",
    "        print(\"当前拥有%.1f个加密货币\" % (balance))\n",
    "    \n",
    "    def print_blockchain(self):\n",
    "        print(\"区块链包含区块个数: %d\\n\" % len(self.blockchain.blocks))\n",
    "        for block in self.blockchain.blocks:\n",
    "            print(\"上个区块哈希：%s\" % block.prev_hash)\n",
    "            print(\"区块内容：%s\" % block.transactions)\n",
    "            print(\"区块哈希：%s\" % block.hash)\n",
    "            print(\"\\n\") "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 初始化节点1\n",
    "\n",
    "node1 = Node(\"节点1\", 8000)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成创世区块\n",
      "节点1 运行中...\n"
     ]
    }
   ],
   "source": [
    "node1.start()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "区块链包含区块个数: 1\n",
      "\n",
      "上个区块哈希：\n",
      "区块内容：[+welFYuuTR5XxItoPwrm7Pue+EhBVJe4HMQ5lpbWstw= 挖矿获取1个加密货币]\n",
      "区块哈希：00000253dbf5699116a1d8d948fb8da76bb12f41f696a9ebe87c2624ecd55c2b\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "node1.print_blockchain()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "node2 = Node(\"节点2\", 8001)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "节点2 发送初始化请求 节点1\n",
      "节点1 处理请求内容...\n",
      "节点2 初始化完成.\n",
      "节点2 运行中...\n"
     ]
    }
   ],
   "source": [
    "node2.start()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "区块链包含区块个数: 1\n",
      "\n",
      "上个区块哈希：\n",
      "区块内容：[+welFYuuTR5XxItoPwrm7Pue+EhBVJe4HMQ5lpbWstw= 挖矿获取1个加密货币]\n",
      "区块哈希：00000253dbf5699116a1d8d948fb8da76bb12f41f696a9ebe87c2624ecd55c2b\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "node2.print_blockchain()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "当前拥有1.0个加密货币\n"
     ]
    }
   ],
   "source": [
    "node1.get_balance()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "当前拥有0.0个加密货币\n"
     ]
    }
   ],
   "source": [
    "node2.get_balance()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_transaction = Transaction(\n",
    "    sender=node1.wallet.address,\n",
    "    recipient=node2.wallet.address,\n",
    "    amount=0.3\n",
    ")\n",
    "sig = node1.wallet.sign(str(new_transaction))\n",
    "new_transaction.set_sign(sig, node1.wallet.pubkey)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "节点1 忽略自身节点\n",
      "节点1 广播新区块至 localhost:8000\n",
      "节点2 处理请求内容...\n",
      "处理交易请求...\n",
      "节点2 验证交易成功\n",
      "节点2 生成新的区块...\n",
      "节点2 将新区块添加到区块链中\n",
      "节点2 将新区块广播到网络中...\n",
      "节点2 广播新区块至 节点1\n",
      "节点1 处理请求内容...\n",
      "节点2处理新区块请求...\n",
      "节点1 区块验证成功\n",
      "节点1  忽略自身节点\n",
      "添加新区块成功\n"
     ]
    }
   ],
   "source": [
    "node1.submit_transaction(new_transaction)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "区块链包含区块个数: 2\n",
      "\n",
      "上个区块哈希：\n",
      "区块内容：[+welFYuuTR5XxItoPwrm7Pue+EhBVJe4HMQ5lpbWstw= 挖矿获取1个加密货币]\n",
      "区块哈希：00000253dbf5699116a1d8d948fb8da76bb12f41f696a9ebe87c2624ecd55c2b\n",
      "\n",
      "\n",
      "上个区块哈希：\n",
      "区块内容：[从 +welFYuuTR5XxItoPwrm7Pue+EhBVJe4HMQ5lpbWstw= 转至 6SM6EU514L4h0W4WufIKR2ISva7Lz/DN1C0OWBYhqQ8= 0个加密货币, 6SM6EU514L4h0W4WufIKR2ISva7Lz/DN1C0OWBYhqQ8= 挖矿获取1个加密货币]\n",
      "区块哈希：00000c7e955e08c0ea8312982f381f5fa73640079a0bb2e1e54503d5ec618ee7\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "node1.print_blockchain()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "区块链包含区块个数: 2\n",
      "\n",
      "上个区块哈希：\n",
      "区块内容：[+welFYuuTR5XxItoPwrm7Pue+EhBVJe4HMQ5lpbWstw= 挖矿获取1个加密货币]\n",
      "区块哈希：00000253dbf5699116a1d8d948fb8da76bb12f41f696a9ebe87c2624ecd55c2b\n",
      "\n",
      "\n",
      "上个区块哈希：\n",
      "区块内容：[从 +welFYuuTR5XxItoPwrm7Pue+EhBVJe4HMQ5lpbWstw= 转至 6SM6EU514L4h0W4WufIKR2ISva7Lz/DN1C0OWBYhqQ8= 0个加密货币, 6SM6EU514L4h0W4WufIKR2ISva7Lz/DN1C0OWBYhqQ8= 挖矿获取1个加密货币]\n",
      "区块哈希：00000c7e955e08c0ea8312982f381f5fa73640079a0bb2e1e54503d5ec618ee7\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "node2.print_blockchain()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "当前拥有0.7个加密货币\n"
     ]
    }
   ],
   "source": [
    "node1.get_balance()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "当前拥有1.3个加密货币\n"
     ]
    }
   ],
   "source": [
    "node2.get_balance()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
